/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * JSONNode.java
 * Copyright (C) 2009-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.core.json;

import java_cup.runtime.DefaultSymbolFactory;
import java_cup.runtime.SymbolFactory;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import java.awt.BorderLayout;
import java.io.Reader;

/**
 * Container class for storing a
 * <a href="http://www.json.org/" target="_blank">JSON</a> data structure.
 * 
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class JSONNode extends DefaultMutableTreeNode {

    /** for serialization. */
    private static final long serialVersionUID = -3047440914507883491L;

    /**
     * The type of a node.
     * 
     * @author FracPete (fracpete at waikato dot ac dot nz)
     * @version $Revision$
     */
    public static enum NodeType {
        /** a primitive. */
        PRIMITIVE,
        /** an object with nested key-value pairs. */
        OBJECT,
        /** an array. */
        ARRAY
    }

    /** the name of the node. */
    protected String m_Name;

    /** the value of the node. */
    protected Object m_Value;

    /** the type of the node. */
    protected NodeType m_NodeType;

    /**
     * Initializes the root container.
     */
    public JSONNode() {
        this(null, NodeType.OBJECT);
    }

    /**
     * Initializes the primitive container.
     * 
     * @param name  the name
     * @param value the primitive value
     */
    public JSONNode(String name, Boolean value) {
        this(name, value, NodeType.PRIMITIVE);
    }

    /**
     * Initializes the primitive container.
     * 
     * @param name  the name
     * @param value the primitive value
     */
    public JSONNode(String name, Integer value) {
        this(name, value, NodeType.PRIMITIVE);
    }

    /**
     * Initializes the primitive container.
     * 
     * @param name  the name
     * @param value the primitive value
     */
    public JSONNode(String name, Double value) {
        this(name, value, NodeType.PRIMITIVE);
    }

    /**
     * Initializes the primitive container.
     * 
     * @param name  the name
     * @param value the primitive value
     */
    public JSONNode(String name, String value) {
        this(name, value, NodeType.PRIMITIVE);
    }

    /**
     * Initializes the object container with null value.
     * 
     * @param name the name
     * @param type the node type
     */
    protected JSONNode(String name, NodeType type) {
        this(name, null, type);
    }

    /**
     * Initializes the container.
     * 
     * @param name  the name
     * @param value the primitive value
     * @param type  the type of the node, null for primitives
     */
    protected JSONNode(String name, Object value, NodeType type) {
        super();

        m_Name = name;
        m_Value = value;
        m_NodeType = type;
    }

    /**
     * Checks whether the node is anonymous.
     * 
     * @return true if no name available
     */
    public boolean isAnonymous() {
        return (m_Name == null);
    }

    /**
     * Returns the name of the node.
     * 
     * @return the name, null for anonymous nodes
     */
    public String getName() {
        return m_Name;
    }

    /**
     * Returns the stored value.
     * 
     * @return the stored value, can be null
     */
    public Object getValue() {
        return getValue(null);
    }

    /**
     * Returns the stored value.
     * 
     * @param defValue the default value, if value is null
     * @return the stored value, can be null
     */
    public Object getValue(Object defValue) {
        if (m_Value == null) {
            return defValue;
        } else {
            if (m_Value instanceof String) {
                return unescape(m_Value.toString());
            } else {
                return m_Value;
            }
        }
    }

    /**
     * Returns whether the node stores a primitive value or a an array/object.
     * 
     * @return true if a primitive, false in case of an array/object
     */
    public boolean isPrimitive() {
        return (m_NodeType == NodeType.PRIMITIVE);
    }

    /**
     * Returns wether the node is an array.
     * 
     * @return true if the node is array container
     */
    public boolean isArray() {
        return (m_NodeType == NodeType.ARRAY);
    }

    /**
     * Returns wether the node is an object.
     * 
     * @return true if the node is object container
     */
    public boolean isObject() {
        return (m_NodeType == NodeType.OBJECT);
    }

    /**
     * Returns the type of the container.
     * 
     * @return the type
     */
    public NodeType getNodeType() {
        return m_NodeType;
    }

    /**
     * Adds a "null" child to the object.
     * 
     * @param name the name of the null value
     * @return the new node, or null if none added
     */
    public JSONNode addNull(String name) {
        return add(name, null, NodeType.PRIMITIVE);
    }

    /**
     * Adds a key-value child to the object.
     * 
     * @param name  the name of the pair
     * @param value the value
     * @return the new node, or null if none added
     */
    public JSONNode addPrimitive(String name, Boolean value) {
        return add(name, value, NodeType.PRIMITIVE);
    }

    /**
     * Adds a key-value child to the object.
     * 
     * @param name  the name of the pair
     * @param value the value
     * @return the new node, or null if none added
     */
    public JSONNode addPrimitive(String name, Integer value) {
        return add(name, value, NodeType.PRIMITIVE);
    }

    /**
     * Adds a key-value child to the object.
     * 
     * @param name  the name of the pair
     * @param value the value
     * @return the new node, or null if none added
     */
    public JSONNode addPrimitive(String name, Double value) {
        return add(name, value, NodeType.PRIMITIVE);
    }

    /**
     * Adds a key-value child to the object.
     * 
     * @param name  the name of the pair
     * @param value the value
     * @return the new node, or null if none added
     */
    public JSONNode addPrimitive(String name, String value) {
        return add(name, value, NodeType.PRIMITIVE);
    }

    /**
     * Adds an array child to the object.
     * 
     * @param name the name of the pair
     * @return the new node, or null if none added
     */
    public JSONNode addArray(String name) {
        return add(name, null, NodeType.ARRAY);
    }

    /**
     * Adds a null array element child to the array.
     * 
     * @return the new node, or null if none added
     */
    public JSONNode addNullArrayElement() {
        return add(null, null, NodeType.PRIMITIVE);
    }

    /**
     * Add a key-value object child into the array
     * 
     * @return the newly added node
     */
    public JSONNode addObjectArrayElement() {
        return add(null, null, NodeType.OBJECT);
    }

    /**
     * Adds an array element child to the array.
     * 
     * @param value the value of the element array
     * @return the new node, or null if none added
     */
    public JSONNode addArrayElement(Object value) {
        NodeType type;

        if (getNodeType() != NodeType.ARRAY) {
            return null;
        }

        type = null;

        if (value != null) {
            if (value instanceof Boolean) {
                type = NodeType.PRIMITIVE;
            } else if (value instanceof Integer) {
                type = NodeType.PRIMITIVE;
            } else if (value instanceof Double) {
                type = NodeType.PRIMITIVE;
            } else if (value instanceof String) {
                type = NodeType.PRIMITIVE;
            } else if (value.getClass().isArray()) {
                type = NodeType.ARRAY;
            } else {
                type = NodeType.OBJECT;
            }
        }

        return add(null, value, type);
    }

    /**
     * Adds an object child to the object.
     * 
     * @param name the name of the pair
     * @return the new node, or null if none added
     */
    public JSONNode addObject(String name) {
        return add(name, null, NodeType.OBJECT);
    }

    /**
     * Adds a key-value child to the object.
     * 
     * @param name  the name of the pair
     * @param value the value
     * @param type  the node type, null for primitives
     * @return the new node, or null if none added
     */
    protected JSONNode add(String name, Object value, NodeType type) {
        JSONNode child;

        if (isPrimitive()) {
            return null;
        }

        child = new JSONNode(name, value, type);
        add(child);

        return child;
    }

    /**
     * Checks whether the node has a child with the given name.
     * 
     * @param name the name of the child
     * @return true if child with that name is available
     */
    public boolean hasChild(String name) {
        return (getChild(name) != null);
    }

    /**
     * Returns the child with the given name.
     * 
     * @param name the name of the child
     * @return the child if available, null otherwise
     */
    public JSONNode getChild(String name) {
        JSONNode result;
        JSONNode node;
        int i;

        result = null;

        for (i = 0; i < getChildCount(); i++) {
            node = (JSONNode) getChildAt(i);
            if (!node.isAnonymous() && node.getName().equals(name)) {
                result = node;
                break;
            }
        }

        return result;
    }

    /**
     * Generates the indentation string.
     * 
     * @param level the level
     * @return the indentation string (tabs)
     */
    protected String getIndentation(int level) {
        StringBuffer result;
        int i;

        result = new StringBuffer();
        for (i = 0; i < level; i++) {
            result.append("\t");
        }

        return result.toString();
    }

    /**
     * Escapes ", \, /, \b, \f, \n, \r, \t in strings.
     * 
     * @param o the object to process (only strings get processed)
     * @return the processed object
     */
    protected Object escape(Object o) {
        if (o instanceof String) {
            return escape((String) o);
        } else {
            return o;
        }
    }

    /**
     * Escapes ", /, \b, \f, \n, \r, \t.
     *
     * @param s the string to process
     * @return the processed
     */
    protected String escape(String s) {
        StringBuffer result;
        int i;
        char c;

        // take care of already escaped characters first (like
        // \n in string literals
        // kind of ugly - probably a better way of handling this
        // somehow.
        s = s.replace("\\n", "@@-@@n").replace("\\r", "@@-@@r").replace("\\t", "@@-@@t").replace("\\b", "@@-@@b").replace("\\f", "@@-@@f");

        if ((s.indexOf('\"') > -1) || (s.indexOf('\\') > -1) || (s.indexOf('\b') > -1) || (s.indexOf('\f') > -1) || (s.indexOf('\n') > -1) || (s.indexOf('\r') > -1) || (s.indexOf('\t') > -1)) {
            result = new StringBuffer();
            for (i = 0; i < s.length(); i++) {
                c = s.charAt(i);
                if (c == '\"') {
                    result.append("\\\"");
                } else if (c == '\\') {
                    result.append("\\\\");
                } else if (c == '\b') {
                    result.append("\\b");
                } else if (c == '\f') {
                    result.append("\\f");
                } else if (c == '\n') {
                    result.append("\\n");
                } else if (c == '\r') {
                    result.append("\\r");
                } else if (c == '\t') {
                    result.append("\\t");
                } else {
                    result.append(c);
                }
            }
        } else {
            result = new StringBuffer(s);
        }

        return result.toString();
    }

    /**
     * Un-escapes ", /, \b, \f, \n, \r, \t.
     *
     * @param s the string to process
     * @return the processed string
     */
    protected String unescape(String s) {
        StringBuilder newStringBuffer;
        int index;

        // replace each of the following characters with the backquoted version
        String charsFind[] = { "\\\\", "\\'", "\\t", "\\n", "\\r", "\\b", "\\f", "\\\"", "\\%", "\\u001E" };
        char charsReplace[] = { '\\', '\'', '\t', '\n', '\r', '\b', '\f', '"', '%', '\u001E' };
        int pos[] = new int[charsFind.length];
        int curPos;

        String str = new String(s);
        newStringBuffer = new StringBuilder();
        while (str.length() > 0) {
            // get positions and closest character to replace
            curPos = str.length();
            index = -1;
            for (int i = 0; i < pos.length; i++) {
                pos[i] = str.indexOf(charsFind[i]);
                if ((pos[i] > -1) && (pos[i] < curPos)) {
                    index = i;
                    curPos = pos[i];
                }
            }

            // replace character if found, otherwise finished
            if (index == -1) {
                newStringBuffer.append(str);
                str = "";
            } else {
                newStringBuffer.append(str.substring(0, pos[index]));
                newStringBuffer.append(charsReplace[index]);
                str = str.substring(pos[index] + charsFind[index].length());
            }
        }

        return newStringBuffer.toString().replace("@@-@@", "\\");
    }

    /**
     * Dumps the node structure into JSON format.
     * 
     * @param buffer the buffer to add the data to
     */
    public void toString(StringBuffer buffer) {
        int level;
        boolean isLast;
        String indent;
        int i;

        level = getLevel();
        isLast = (getNextSibling() == null);
        indent = getIndentation(level);

        buffer.append(indent);
        if (m_Name != null) {
            buffer.append("\"");
            buffer.append(escape(m_Name));
            buffer.append("\" : ");
        }

        if (isObject()) {
            buffer.append("{\n");
            for (i = 0; i < getChildCount(); i++) {
                ((JSONNode) getChildAt(i)).toString(buffer);
            }
            buffer.append(indent);
            buffer.append("}");
        } else if (isArray()) {
            buffer.append("[\n");
            for (i = 0; i < getChildCount(); i++) {
                ((JSONNode) getChildAt(i)).toString(buffer);
            }
            buffer.append(indent);
            buffer.append("]");
        } else {
            if (m_Value == null) {
                buffer.append("null");
            } else if (m_Value instanceof String) {
                buffer.append("\"");
                buffer.append(escape((String) m_Value));
                buffer.append("\"");
            } else {
                buffer.append(m_Value.toString());
            }
        }

        if (!isLast) {
            buffer.append(",");
        }
        buffer.append("\n");
    }

    /**
     * Returns a string representation of the node.
     * 
     * @return the string representation
     */
    @Override
    public String toString() {
        String result;

        result = null;

        if (isObject()) {
            if (isRoot()) {
                result = "JSON";
            } else if (m_Name == null) {
                result = "<object>";
            } else {
                result = escape(m_Name) + " (Object)";
            }
        } else if (isArray()) {
            if (m_Name == null) {
                result = "<array>";
            } else {
                result = escape(m_Name) + " (Array)";
            }
        } else {
            if (m_Name != null) {
                result = escape(m_Name) + ": " + escape(m_Value);
            } else {
                result = "" + m_Value;
            }
        }

        return result;
    }

    /**
     * Reads the JSON object from the given reader.
     * 
     * @param reader the reader to read the JSON object from
     * @return the generated JSON object
     * @throws Exception if parsing fails
     */
    @SuppressWarnings("deprecation")
    public static JSONNode read(Reader reader) throws Exception {
        SymbolFactory sf;
        Parser parser;

        sf = new DefaultSymbolFactory();
        parser = new Parser(new Scanner(reader, sf), sf);
        parser.parse();

        return parser.getResult();
    }

    /**
     * Only for testing. Generates a simple JSON object and displays it.
     * 
     * @param args ignored
     * @throws Exception if something goes wrong
     */
    public static void main(String[] args) throws Exception {
        // generates the example listed here:
        // http://en.wikipedia.org/wiki/JSON
        JSONNode person = new JSONNode();
        person.addPrimitive("firstName", "John");
        person.addPrimitive("lastName", "Smith");
        JSONNode address = person.addObject("address");
        address.addPrimitive("streetAddress", "21 2nd Street");
        address.addPrimitive("city", "New York");
        address.addPrimitive("state", "NY");
        address.addPrimitive("postalCode", 10021);
        JSONNode phonenumbers = person.addArray("phoneNumbers");
        phonenumbers.addArrayElement("212 555-1234");
        phonenumbers.addArrayElement("646 555-4567");

        // output in console
        StringBuffer buffer = new StringBuffer();
        person.toString(buffer);
        System.out.println(buffer.toString());

        // display GUI
        JTree tree = new JTree(person);
        JFrame frame = new JFrame("JSON");
        frame.setSize(800, 600);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(new JScrollPane(tree), BorderLayout.CENTER);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}
